Skip to content

feat(matrix): support (array), (string) and (bool) casts via engine handlers - #17

Merged
lisachenko merged 7 commits into
masterfrom
claude/casting-operator-jukdna
Aug 7, 2026
Merged

feat(matrix): support (array), (string) and (bool) casts via engine handlers#17
lisachenko merged 7 commits into
masterfrom
claude/casting-operator-jukdna

Conversation

@lisachenko

@lisachenko lisachenko commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Closes #9.

What this does

Matrix implements ObjectCastInterface::__cast() and ObjectGetPropertiesForInterface::__getFields(), dispatched by the engine's cast_object / get_properties_for handlers:

  • (string) $matrix → pretty-printed rows, one per line: [1, 2, 3]
  • (bool) $matrixtrue (a valid matrix is never empty by construction)
  • (array) $matrixtoArray() — array casts arrive at get_properties_for with the ArrayCast purpose, not at cast_object (the CastType::Array branch in __cast stays for engine paths that pass IS_ARRAY directly)
  • numeric casts fall through to the engine default: warning Object of class ... could not be converted to int/float plus the substitute value 1 / 1.0, exactly as with no handler installed
  • every other get_properties_for purpose (debug, serialize, var_export, JSON) reproduces the default property table via get_mangled_object_vars(), so var_dump() / serialize() / var_export() / json_encode() output is byte-identical to before (covered by tests)
  • GET_OBJECT_VARS purpose serves the publicly visible entries only — the engine skips visibility filtering entirely once a custom handler is installed, so without this get_object_vars($m) would leak the mangled private table. Known deviation: class-scoped callers (a closure bound to Matrix) also get the public view, since the calling scope is not recoverable inside the FFI callback; documented and pinned by its own .phpt.

Neither hook ever throws — they run inside FFI callbacks, where PHP escalates any exception into a fatal engine error.

All dispatch is on z-engine's named CastType / PropertyPurpose enums (getCastTypeEnum() / getPurposeEnum()), not numeric constants — the values are guarded upstream against the generated engine ground truth and adapt per z-engine line, which is what lets the same code run on 8.4 and 8.5.

Upstream work this PR surfaced (both merged)

Implementing this uncovered two z-engine bugs, fixed on the 8.4 branch and merged up to master:

Earlier revisions of this branch carried local workarounds for both (hand-declared ENGINE_IS_BOOL/PROP_PURPOSE_* constants, a hand-rolled substitute-value fallback); the final revision drops them for the upstream API.

Rebased onto multi-version master

Per review feedback, the branch is rebased onto the php: ^8.4 + z-engine: 8.4.x-dev || 8.5.x-dev master — no tag dependency. Verified with fresh installs on both minors:

  • PHP 8.4.19 (z-engine 8.4.x-dev): 19/19 tests, PHPStan level max clean, PER-CS2.0 clean
  • PHP 8.5.9 (z-engine dev-master): 19/19 tests, PHPStan level max clean

Tests

Seven new .phpt files (all with the mandatory three-line --INI-- section): array/string/bool casts, the numeric fall-through (asserting the restored engine warning via --EXPECTREGEX--), plus regression locks for var_dump() output and get_object_vars() visibility (outside and class-scoped).


🤖 Generated with Claude Code

https://claude.ai/code/session_01JsbdqisRfGuujN3QD9n8En

@lisachenko

lisachenko commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Rebase and don’t wait for 8.4.1 tag - use what is inside master branch of native-matrix now - multiple versions of z-engine and wide php:^8.4 (this should come from rebase)

claude added 5 commits August 7, 2026 22:25
…andlers

Implements ObjectCastInterface::__cast() dispatching on the cast type:
pretty-printed rows for (string), true for (bool) — a valid matrix is
never empty by construction — and toArray() for IS_ARRAY. Unsupported
cast types fall through to $hook->proceed(); when the default engine
handler reports failure (numeric casts write no value), the handler
substitutes the engine's own documented fallback of 1/1.0 because the
z-engine trampoline always reports success to the engine. The handler
runs across the FFI boundary and therefore never throws.

Two PHP 8.4 realities shape the implementation:

- PHP 8.1 inserted IS_NEVER = 17 into the engine type table, shifting
  _IS_BOOL to 18 and _IS_NUMBER to 19; z-engine dev-master still
  declares the pre-8.1 values, so boolean casts are matched against a
  local ENGINE_IS_BOOL = 18 constant instead of the stale
  ReflectionValue::_IS_BOOL.
- (array) casts do not reach cast_object at all: the engine routes them
  through get_properties_for with the ARRAY_CAST purpose. Matrix now
  also implements ObjectGetPropertiesForInterface::__getFields(),
  returning the rows for array casts while reproducing the default
  property table for debugging, serialization, var_export and JSON so
  their output stays byte-identical. get_object_vars() exposes only
  publicly visible entries because the engine skips visibility
  filtering once a custom handler is installed.

CastObjectHook::getResult() is deliberately never called after a failed
proceed(): the retval slot is uninitialized scratch memory in that case
and reading it corrupts the calling VM frame.

Closes #9

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsbdqisRfGuujN3QD9n8En
…ies hooks

Review follow-up: the silently-absent engine warning on numeric cast
fallback, the scope-insensitive public view served to get_object_vars()
callers, and the getResult()-only-after-success rule are all deliberate;
say so where a future change would otherwise "fix" them. Also corrects
the stale-constants note to reference the z-engine 8.4 line instead of
dev-master — composer.json already pins the stable 8.4.0 tag — and
imports ARRAY_FILTER_USE_KEY for consistency with the function imports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsbdqisRfGuujN3QD9n8En
The scope-insensitivity of the get_properties_for hook is a documented
deviation from default engine behaviour; lock it in a test so a change
there is a conscious decision, and name the intentionally absent engine
warning in the numeric-fallback test title.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsbdqisRfGuujN3QD9n8En
…-through

z-engine 8.4.1 (PRs lisachenko/z-engine#155 and #156) ships the API the
local workarounds stood in for: CastType/PropertyPurpose enums exposed
via getCastTypeEnum()/getPurposeEnum(), guarded upstream against the
generated engine ground truth, and a CastObjectHook whose fall-through
behaves exactly like an uninstalled handler. Drop the local
ENGINE_IS_BOOL/ENGINE_IS_NUMBER/PROP_PURPOSE_* constants — values that
can silently drift between PHP minors — and dispatch on the named cases
instead.

The numeric-cast fallback is gone entirely: __cast now defers to
proceed()/getResult(), and the failed cast propagates to the engine
caller, which emits its own "could not be converted to int/float"
warning and substitutes 1 — the previously undeliverable default
diagnostic is restored, and the fallback test asserts it.

Raises the z-engine floor to ~8.4.1 accordingly, in lockstep with the
PHP 8.4 pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsbdqisRfGuujN3QD9n8En
@lisachenko
lisachenko force-pushed the claude/casting-operator-jukdna branch from 368684c to 8896331 Compare August 7, 2026 22:32

Copy link
Copy Markdown
Owner Author

Done — rebased onto master, so the branch now carries php: ^8.4 with z-engine: 8.4.x-dev || 8.5.x-dev (the ~8.4.1 tag bump dropped out in the rebase). Verified with fresh installs on both minors: PHP 8.4.19 resolves 8.4.x-dev and PHP 8.5.9 resolves dev-master, 19/19 tests plus PHPStan level max green on each. The enum-based dispatch (CastType/PropertyPurpose) needed no per-minor changes — the named cases carry the correct ids on both lines.


Generated by Claude Code

Comment thread src/Matrix.php Outdated
claude added 2 commits August 7, 2026 22:36
With the suite running on PHP 8.4 and 8.5 in parallel, a version-gated
skip could silently shrink coverage on one leg while the job stays
green. failOnSkipped/failOnIncomplete in phpunit.xml.dist turn any
skipped or incomplete test into a failure for both CI legs and local
runs alike.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsbdqisRfGuujN3QD9n8En
Review follow-up: the cast fall-through docblock still promised the
behaviour "with z-engine >= 8.4.1", but the package consumes the
8.4.x-dev/8.5.x-dev branches now - no tagged version to point at.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsbdqisRfGuujN3QD9n8En
@lisachenko
lisachenko marked this pull request as ready for review August 7, 2026 22:39
@lisachenko
lisachenko merged commit 2757314 into master Aug 7, 2026
5 checks passed
@lisachenko
lisachenko deleted the claude/casting-operator-jukdna branch August 7, 2026 22:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: scalar casts (string) / (array) / (bool) via ObjectCastInterface

2 participants